package com.tapadoo.slacknotifier;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import jetbrains.buildServer.issueTracker.Issue;
import jetbrains.buildServer.serverSide.*;
import jetbrains.buildServer.serverSide.settings.ProjectSettingsManager;
import jetbrains.buildServer.users.SUser;
import jetbrains.buildServer.users.UserSet;
import jetbrains.buildServer.vcs.SelectPrevBuildPolicy;
import jetbrains.buildServer.vcs.VcsRoot;
import org.joda.time.Duration;
import org.joda.time.Period;
import org.joda.time.format.PeriodFormatter;
import org.joda.time.format.PeriodFormatterBuilder;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collection;
/**
* Created by jasonconnery on 02/03/2014.
*/
public class SlackServerAdapter extends BuildServerAdapter {
private final SBuildServer buildServer;
private final SlackConfigProcessor slackConfig;
private final ProjectSettingsManager projectSettingsManager;
private final ProjectManager projectManager ;
private Gson gson ;
public SlackServerAdapter(SBuildServer sBuildServer, ProjectManager projectManager, ProjectSettingsManager projectSettingsManager , SlackConfigProcessor configProcessor) {
this.projectManager = projectManager ;
this.projectSettingsManager = projectSettingsManager ;
this.buildServer = sBuildServer ;
this.slackConfig = configProcessor ;
}
public void init()
{
buildServer.addListener(this);
}
private Gson getGson()
{
if( gson == null )
{
gson = new GsonBuilder().create() ;
}
return gson ;
}
@Override
public void buildStarted(SRunningBuild build) {
super.buildStarted(build);
if( !build.isPersonal() && slackConfig.postStarted() )
{
postStartedBuild(build);
}
}
@Override
public void buildFinished(SRunningBuild build) {
super.buildFinished(build);
if( !build.isPersonal() && build.getBuildStatus().isSuccessful() && slackConfig.postSuccessful() )
{
processSuccessfulBuild(build);
}
else if ( !build.isPersonal() && build.getBuildStatus().isFailed() && slackConfig.postFailed() )
{
postFailureBuild(build);
}
else
{
//TODO - modify in future if we care about other states
}
}
private void postStartedBuild(SRunningBuild build )
{
//Could put other into here. Agents maybe?
String message = String.format("Project '%s' build started." , build.getFullName());
postToSlack(build, message, true);
}
private void postFailureBuild(SRunningBuild build )
{
String message = "";
PeriodFormatter durationFormatter = new PeriodFormatterBuilder()
.printZeroRarelyFirst()
.appendHours()
.appendSuffix(" hour", " hours")
.appendSeparator(" ")
.printZeroRarelyLast()
.appendMinutes()
.appendSuffix(" minute", " minutes")
.appendSeparator(" and ")
.appendSeconds()
.appendSuffix(" second", " seconds")
.toFormatter();
Duration buildDuration = new Duration(1000*build.getDuration());
message = String.format("Project '%s' build failed! ( %s )" , build.getFullName() , durationFormatter.print(buildDuration.toPeriod()));
postToSlack(build, message, false);
}
private void processSuccessfulBuild(SRunningBuild build) {
String message = "";
PeriodFormatter durationFormatter = new PeriodFormatterBuilder()
.printZeroRarelyFirst()
.appendHours()
.appendSuffix(" hour", " hours")
.appendSeparator(" ")
.printZeroRarelyLast()
.appendMinutes()
.appendSuffix(" minute", " minutes")
.appendSeparator(" and ")
.appendSeconds()
.appendSuffix(" second", " seconds")
.toFormatter();
Duration buildDuration = new Duration(1000*build.getDuration());
message = String.format("Project '%s' built successfully in %s." , build.getFullName() , durationFormatter.print(buildDuration.toPeriod()));
postToSlack(build, message, true);
}
/**
* Post a payload to slack with a message and good/bad color. Committer summary is automatically added as an attachment
* @param build the build the message is relating to
* @param message main message to include, 'Build X completed...' etc
* @param goodColor true for 'good' builds, false for danger.
*/
private void postToSlack(SRunningBuild build, String message, boolean goodColor) {
try{
URL url = new URL(slackConfig.getPostUrl());
SlackProjectSettings projectSettings = (SlackProjectSettings) projectSettingsManager.getSettings(build.getProjectId(),"slackSettings");
if( ! projectSettings.isEnabled() )
{
return ;
}
String iconUrl = projectSettings.getLogoUrl();
if(iconUrl == null || iconUrl.length() < 1 )
{
iconUrl = slackConfig.getLogoUrl() ;
}
String configuredChannel = build.getParametersProvider().get("SLACK_CHANNEL");
String channel = this.slackConfig.getDefaultChannel();
if( configuredChannel != null && configuredChannel.length() > 0 )
{
channel = configuredChannel ;
}
else if( projectSettings != null && projectSettings.getChannel() != null && projectSettings.getChannel().length() > 0 )
{
channel = projectSettings.getChannel() ;
}
UserSet<SUser> committers = build.getCommitters(SelectPrevBuildPolicy.SINCE_LAST_BUILD);
StringBuilder committersString = new StringBuilder();
for( SUser committer : committers.getUsers() )
{
if( committer != null)
{
String committerName = committer.getName() ;
if( committerName == null || committerName.equals("") )
{
committerName = committer.getUsername() ;
}
if( committerName != null && !committerName.equals(""))
{
committersString.append(committerName);
committersString.append(",");
}
}
}
if( committersString.length() > 0 )
{
committersString.deleteCharAt(committersString.length()-1); //remove the last ,
}
String commitMsg = committersString.toString();
JsonObject payloadObj = new JsonObject();
payloadObj.addProperty("channel" , channel);
payloadObj.addProperty("username" , "TeamCity");
payloadObj.addProperty("text", message);
payloadObj.addProperty("icon_url",iconUrl);
JsonArray attachmentsObj = new JsonArray();
if( commitMsg.length() > 0 )
{
JsonObject attachment = new JsonObject();
attachment.addProperty("fallback", "Changes by"+ commitMsg);
attachment.addProperty("color",( goodColor ? "good" : "danger"));
JsonArray fields = new JsonArray();
JsonObject field = new JsonObject() ;
field.addProperty("title","Changes By");
field.addProperty("value",commitMsg);
field.addProperty("short", true);
fields.add(field);
attachment.add("fields",fields);
attachmentsObj.add(attachment);
}
//Do we have any issues?
if( build.isHasRelatedIssues() )
{
//We do!
Collection<Issue> issues = build.getRelatedIssues();
JsonObject issuesAttachment = new JsonObject();
StringBuilder issueIds = new StringBuilder();
StringBuilder clickableIssueIds = new StringBuilder();
for( Issue issue : issues )
{
issueIds.append(',');
issueIds.append(issue.getId());
clickableIssueIds.append(',');
clickableIssueIds.append('<');
clickableIssueIds.append(issue.getUrl());
clickableIssueIds.append('|');
clickableIssueIds.append(issue.getId());
clickableIssueIds.append('>');
}
if( issueIds.length() > 0 )
{
issueIds.deleteCharAt(0); //delete first ','
}
if( clickableIssueIds.length() > 0 )
{
clickableIssueIds.deleteCharAt(0); //delete first ','
}
issuesAttachment.addProperty("fallback" , "Issues " + issueIds.toString());
//Not sure what color, if any to use for this. For now, leave it the same as the committers one
issuesAttachment.addProperty("color",( goodColor ? "good" : "danger"));
JsonArray fields = new JsonArray();
JsonObject field = new JsonObject() ;
field.addProperty("title","Related Issues");
field.addProperty("value",clickableIssueIds.toString());
field.addProperty("short", true);
fields.add(field);
issuesAttachment.add("fields", fields);
attachmentsObj.add(issuesAttachment);
}
if( attachmentsObj.size() > 0 ) {
payloadObj.add("attachments", attachmentsObj);
}
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setDoOutput(true);
BufferedOutputStream bos = new BufferedOutputStream(conn.getOutputStream());
String payloadJson = getGson().toJson(payloadObj);
String bodyContents = "payload=" + payloadJson ;
bos.write(bodyContents.getBytes("utf8"));
bos.flush();
bos.close();
int serverResponseCode = conn.getResponseCode() ;
conn.disconnect();
conn = null ;
url = null ;
}
catch ( MalformedURLException ex )
{
} catch (IOException e) {
e.printStackTrace();
}
}
}